﻿/*
 * KLF Headless Browser
 * Written by Kris Oye <kristianoye@gmail.com>
 * Copyright (C) 2011-2012.  All rights reserved.
 */
namespace KLF.Lib.Browser.DOM.HTML
{
    #region Using Statements

    using System;
    using System.Linq;
    using KLF.Lib.Browser.DOM.CSS2;
    using KLF.Lib.Browser.DOM.Events;
    using KLF.Lib.Browser.DOM.Parser;
    using KLF.Lib.Browser.DOM.Interface;
    using KLF.Lib.Browser.DOM.CSS.SAC;
    using System.Reflection;
using KLF.Lib.Browser.DOM.CSS.SAC.Selectors;
    using Antlr.Runtime;
    using KLF.Lib.Browser.DOM.CSS;
    using KLF.Lib.Browser.Events;

    #endregion

    public class HTMLElement : Element, NodeSelector, IEventTarget
    {
        #region Constructors

        internal HTMLElement(Document owner, string tagName)
            : base(owner, tagName)
        {
        }

        #endregion

        #region DOM Level 1 Compliance

        public override Node appendChild(Node child)
        {
            if ((child is HTMLTableRowElement) && !(this is HTMLTableSectionElement))
            {
                HTMLTableRowElement tr = child as HTMLTableRowElement;
                HTMLElement p = this;

                do
                {
                    if (p is HTMLTableElement)
                    {
                        HTMLTableElement t = p as HTMLTableElement;
                        HTMLTableSectionElement body = t.lastBodyElement;

                        if (body == null)
                        {
                            body = (HTMLTableSectionElement)Owner
                               .createElement(HTMLConstants.HTML_TAG_TABLEBODY);
                            p.appendChild(body);
                        }
                        body.appendChild(child);
                        return child;
                    }
                    else if (p is HTMLTableSectionElement)
                    {
                        break;
                    }
                    else if (p.Parent != null)
                    {
                        p = p.Parent as HTMLElement;
                    }
                }
                while (p != null);
            }
            return base.appendChild(child);
        }

        #endregion

        #region DOM Level 2 HTML Compliance

        /// <summary>
        /// Simulate the unfocus event
        /// </summary>
        public void blur()
        {
            FocusEvent e = (FocusEvent)Owner
                .createEvent(HTMLConstants.HTML_EVENT_FOCUSEVENT);

            e.initFocusEvent(
                HTMLConstants.HTML_EVENT_BLUR,
                false,
                false,
                Owner.Context.Window,
                0,
                null);
            e.target = this;
            dispatchEvent(e);
        }

        /// <summary>
        /// Double click event
        /// </summary>
        public void dblclick()
        {
            focus();

            MouseEvent e = (MouseEvent)Owner.createEvent(HTMLConstants.HTML_EVENT_HTMLEVENTS);
            e.initMouseEvent(HTMLConstants.HTML_EVENT_DBLCLICK,
                true,
                true,
                null,
                0,
                1,
                1,
                1,
                1,
                false,
                false,
                false,
                false,
                0,
                this);
            e.type = HTMLConstants.HTML_EVENT_DBLCLICK;
            e.target = this;
            dispatchEvent(e);
        }

        /// <summary>
        /// The class attribute of the element. This attribute has been 
        /// renamed due to conflicts with the "class" keyword exposed by 
        /// many languages.
        /// </summary>
        public string className
        {
            get
            {
                return getAttribute(HTMLConstants.HTML_ATT_CLASS);
            }
            set
            {
                setAttribute(HTMLConstants.HTML_ATT_CLASS, value);
            }
        }

        /// <summary>
        /// Simulates a click.
        /// </summary>
        public virtual void click()
        {
            //  The mouse moves over the element.
            MouseEvent mouseOver = (MouseEvent)Owner.createEvent(HTMLConstants.HTML_EVENT_MOUSEEVENT);
            mouseOver.initMouseEvent(HTMLConstants.HTML_EVENT_ON_MOUSEOVER,
                true,
                true,
                null,
                0,
                1,
                1,
                1,
                1,
                false,
                false,
                false,
                false,
                0,
                this);
            mouseOver.target = this;
            dispatchEvent(mouseOver);

            //  The mouse button is clicked.
            MouseEvent mouseDown = (MouseEvent)Owner.createEvent(HTMLConstants.HTML_EVENT_MOUSEEVENT);
            mouseDown.initMouseEvent(HTMLConstants.HTML_EVENT_ON_MOUSEDOWN,
                true,
                true,
                null,
                0,
                1,
                1,
                1,
                1,
                false,
                false,
                false,
                false,
                0,
                this);
            mouseDown.target = this;
            dispatchEvent(mouseDown);

            //  The mouse button is clicked.
            MouseEvent mouseUp = (MouseEvent)Owner.createEvent(HTMLConstants.HTML_EVENT_MOUSEEVENT);
            mouseUp.initMouseEvent(HTMLConstants.HTML_EVENT_ON_MOUSEUP,
                true,
                true,
                null,
                0,
                1,
                1,
                1,
                1,
                false,
                false,
                false,
                false,
                0,
                this);
            mouseUp.target = this;
            dispatchEvent(mouseUp);

            focus();

            //  The mouse click is complete.
            MouseEvent mouseClick = (MouseEvent)Owner.createEvent(HTMLConstants.HTML_EVENT_MOUSEEVENT);
            mouseClick.initMouseEvent(HTMLConstants.HTML_EVENT_CLICK,
                true,
                true,
                null,
                0,
                1,
                1,
                1,
                1,
                false,
                false,
                false,
                false,
                0,
                this);
            mouseClick.target = this;
            dispatchEvent(mouseClick);
        }

        /// <summary>
        /// Specifies the base direction of directionally neutral text and the
        /// directionality of tables.
        /// </summary>
        public string dir
        {
            get
            {
                return getAttribute(HTMLConstants.HTML_ATT_DIR);
            }
            set
            {
                setAttribute(HTMLConstants.HTML_ATT_DIR, value);
            }
        }

        /// <summary>
        /// Simulates a focus event.
        /// </summary>
        public void focus()
        {
            FocusEvent focusIn = (FocusEvent)Owner
                .createEvent(HTMLConstants.HTML_EVENT_FOCUSEVENT);
            focusIn.initFocusEvent(
                HTMLConstants.HTML_EVENT_FOCUS_IN,
                true,
                false,
                Owner.Context.Window,
                0,
                Owner.activeElement);
            focusIn.target = this;
            dispatchEvent(focusIn);

            if (Owner.activeElement != null)
                Owner.activeElement.blur();

            FocusEvent e = (FocusEvent) Owner.createEvent(HTMLConstants.HTML_EVENT_FOCUSEVENT);
            e.initFocusEvent(
                HTMLConstants.HTML_EVENT_FOCUS,
                false,
                false,
                Owner.Context.Window, 
                0, 
                null);

            Owner.activeElement = this;
            e.target = this;
            dispatchEvent(e);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="attName"></param>
        public override string getAttribute(string attName)
        {
            if (Owner is XHTMLDocument)
                return base.getAttribute(attName);
            else
            {
                Attr result = Attributes
                    .DefaultIfEmpty(null)
                    .Cast<Attr>()
                    .FirstOrDefault(n =>
                    {
                        Attr a = n as Attr;
                        return (a != null && a.Name.ToLowerInvariant()
                            .Equals(attName.ToLowerInvariant()));
                    });

                if (result != null)
                    return result.Value;
                else
                    return "";
            }
        }

        /// <summary>
        /// The element's identifier.
        /// </summary>
        public string id
        {
            get
            {
                return getAttribute(HTMLConstants.HTML_ATT_ID);
            }
            set
            {
                setAttribute(HTMLConstants.HTML_ATT_ID, value);
            }
        }

        /// <summary>
        /// Language code defined in RFC 1766.
        /// </summary>
        public string lang
        {
            get
            {
                return getAttribute(HTMLConstants.HTML_ATT_LANG);
            }
            set
            {
                setAttribute(HTMLConstants.HTML_ATT_LANG, value);
            }
        }

        /// <summary>
        /// Override of the Owner for easier casting.
        /// </summary>
        public new HTMLDocument Owner
        {
            get
            {
                return (HTMLDocument)base.Owner;
            }
            set
            {
                base.Owner = value;
            }
        }

        public override void setAttribute(string name, string value)
        {
            if (name.ToLowerInvariant().StartsWith("on"))
            {
                Type t = this.GetType();
                PropertyInfo pi = t.GetProperty(name.ToLowerInvariant());

                if (pi != null)
                    pi.SetValue(this, (object)value, null);
            }
            else if (name.ToLowerInvariant().Equals(HTMLConstants.HTML_ATT_STYLE))
            {
                try
                {
                    ANTLRStringStream stream = new ANTLRStringStream("* { " + value + " }");
                    CSS21Lexer lexer = new CSS21Lexer(stream);
                    CSS21Parser parser = new CSS21Parser(new CommonTokenStream(lexer));
                    CSSStyleRule rule = parser.getRuleSet() as CSSStyleRule;

                    if (rule != null)
                    {
                        foreach (var item in rule.style.Values)
                        {
                            style._Values[new StringLookup(item.Key).StringIndex] =
                                new StringLookup(item.Value.ToString()).StringIndex;
                        }
                    }
                }
                catch (Exception ex)
                {
                }
            }
            base.setAttribute(name, value);
        }

        /// <summary>
        /// The element's advisory title.
        /// </summary>
        public string title
        {
            get
            {
                return getAttribute(HTMLConstants.HTML_ATT_TITLE);
            }
            set
            {
                setAttribute(HTMLConstants.HTML_ATT_TITLE, value);
            }
        }

        /// <summary>
        /// Attempt to me more like browser-like.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return String.Format("[object {0}]", GetType().Name);
        }

        #endregion

        #region DOM Level 2 Style Compliance

        /// <summary>
        /// Returns a style object.
        /// </summary>
        public CSS2Properties style
        {
            get
            {
                if (_style == null)
                    _style = new CSS2Properties();
                return _style;
            }
            set
            {
                _style = value;
            }
        }
        internal CSS2Properties _style { get; set; }

        #endregion

        #region Non-Standard DOM Methods

        /// <summary>
        /// Inserts the given HTML text into the element at the location.
        /// </summary>
        /// 
        /// <param name="sWhere">Required. String that specifies where to 
        /// insert the HTML text, using one of the following values:</param>
        /// 
        /// <param name="sText"></param>
        public void insertAdjacentHTML(string sWhere, string sText)
        {
            Node n = Owner.createDocumentFragment();
            DocumentBuilder.ParseFragment(sText, Owner, n);
            switch (sWhere.ToLowerInvariant())
            {
                case "beforebegin":
                    break;
                case "afterbegin":
                    break;
                case "beforeend":
                    break;
                case "afterend":
                    break;
            }
        }

        /// <summary>
        /// Returns elements matching the class specification.  This may 
        /// contain multiple classes separated by spaces.
        /// </summary>
        /// <param name="spec"></param>
        /// <returns></returns>
        public NodeList getElementsByClassName(string spec)
        {
            if (!String.IsNullOrEmpty(spec))
            {
                string[] specs = spec.Trim().Split(' ');

                if (specs.Length == 0)
                    return new NodeList(new Node[0]);

                return new NodeList(Elements.Where(e =>
                {
                    var match = e.getAttribute(HTMLConstants.HTML_ATT_CLASS)
                        .Split(' ')
                        .Distinct()
                        .Union(specs);

                    return match.Count() == specs.Length;
                }).Cast<Node>());
            }
            return new NodeList(new Node[0]);
        }

        #endregion

        #region NodeSelector Members

        public Element querySelector(Selector selector)
        {
            return Elements
                .DefaultIfEmpty(null)
                .Cast<HTMLElement>()
                .FirstOrDefault(e => selector.Matches(e as HTMLElement));
        }

        public Element querySelector(string selectorString)
        {
            SACParser p = new SACParser();
            return querySelector(p.ParseSelector(selectorString));
        }

        public NodeList querySelectorAll(string selectorString)
        {
            SACParser p = new SACParser();
            return querySelectorAll(p.ParseSelector(selectorString));
        }

        public NodeList querySelectorAll(Selector sel)
        {
            return new NodeList(Elements
                .Cast<Node>()
                .Where(e => sel.Matches(e as HTMLElement)));
        }

        #endregion

        #region HTML DOM Events

        /// <summary>
        /// Runs if the document loading is aborted.
        /// </summary>
        public object onabort
        {
            get;
            set;
        }

        /// <summary>
        /// Runs after a script executes.
        /// </summary>
        public object onafterscriptexecute
        {
            get;
            set;
        }

        /// <summary>
        /// Runs before a script executes.
        /// </summary>
        public object onbeforescriptexecute
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when the element is blurred.
        /// </summary>
        public object onblur
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when the element changes.
        /// </summary>
        public object onchange
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when the element is clicked on.
        /// </summary>
        public object onclick
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when the element is double-clicked.
        /// </summary>
        public object ondblclick
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when an external file fails to load.
        /// </summary>
        public object onerror
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when the element gets focus.
        /// </summary>
        public object onfocus
        {
            get;
            set;
        }

        /// <summary>
        /// An event handler for the input event on the window. The input event
        /// is raised when an <input> element value changes.
        /// </summary>
        public object oninput
        {
            get;
            set;
        }

        /// <summary>
        /// Fires on the release of a key.
        /// </summary>
        public object onkeydown
        {
            get;
            set;
        }

        /// <summary>
        /// Fires after key down and key up.
        /// </summary>
        public object onkeypress
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when after a key is initially pressed.
        /// </summary>
        public object onkeyup
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when a mouse button is clicked.
        /// </summary>
        public object onmousedown
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when the mouse is moved (not likely)
        /// </summary>
        public object onmousemove
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when the mouse button is released.
        /// </summary>
        public object onmouseup
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when text has been selected from an element.
        /// </summary>
        public object onselect
        {
            get;
            set;
        }

        /// <summary>
        /// Fires when the element looses focus.
        /// </summary>
        public object onunfocus
        {
            get;
            set;
        }

        #endregion
    }
}
